// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.missile;

import bats.scene.Physics;
import bats.scene.SceneNode;
import com.jme3.bullet.PhysicsSpace;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.objects.PhysicsRigidBody;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Spatial;
import java.util.logging.Level;
import java.util.logging.Logger;
import jme3utilities.physics.OverlapListener;
import jme3utilities.physics.SimpleSolidControl;

/**
 * A simple solid control for a shaft in flight. A shaft tends to point in the
 * direction of motion. In the event of a physical collision, transfer control
 * to PenetrateControl.
 * <p>
 * Each instance is disabled at creation.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public class ShaftControl
        extends SimpleSolidControl
        implements OverlapListener {
    // *************************************************************************
    // constants

    /**
     * coefficient of friction for shafts
     */
    final private static float friction = 0.2f;
    /**
     * feedback gain for restorative torques TODO units? variation?
     */
    final private static float restoreGain = 0.003f;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(ShaftControl.class.getName());
    // *************************************************************************
    // fields
    /**
     * which scene the shaft resides in: set by constructor
     */
    final private SceneNode scene;
    /**
     * velocity prior to previous physics tick (world units per second)
     */
    private Vector3f oldVelocity = null;
    // *************************************************************************
    // constructors

    /**
     * Instantiate a disabled control with the specified collision shape and
     * mass.
     *
     * @param shape the shaft's collision shape (not null, CapsuleCollisionShape
     * is good)
     * @param mass the shaft's mass (in kilograms, &gt;0)
     * @param scene which scene the shaft is in (not null)
     */
    public ShaftControl(CollisionShape shape, float mass, SceneNode scene) {
        super(false, shape, mass, scene.getWorld().getPhysics().getSpace());
        assert mass > 0f : mass;

        this.scene = scene;

        assert !isEnabled();
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Enable the control and initialize the object's position and motion.
     *
     * @param location in world coordinates (not null, unaffected)
     * @param orientation in world coordinates (not null, unaffected)
     * @param velocity in world coordinates (world units per second, not null,
     * unaffected)
     */
    public void enable(Vector3f location, Quaternion orientation,
            Vector3f velocity) {
        setPhysicsLocation(location.clone());
        setPhysicsRotation(orientation.clone());

        setLinearVelocity(velocity.clone());
        oldVelocity = velocity.clone();
        setAngularVelocity(new Vector3f());

        setEnabled(true);
    }
    // *************************************************************************
    // OverlapListener methods

    /**
     * React to an overlapping rigid body.
     *
     * @param overlappingBody the overlapping rigid body (not null)
     * @param overlappingSpatial the spatial of the overlapping rigid body (not
     * null)
     * @param localPoint the location of the overlap (rotated and translated to
     * this control's object, but at world scale)
     */
    @Override
    public void onOverlap(PhysicsRigidBody overlappingBody,
            Spatial overlappingSpatial, Vector3f localPoint) {
        assert overlappingBody != null;
        assert overlappingSpatial != null;
        assert localPoint != null;

        if (!isEnabled()) {
            return;
        }
        logger.log(Level.INFO, "{0} overlapped with {1}",
                new Object[]{
            spatial.getName(),
            overlappingSpatial.getName()
        });

        Vector3f pointDirection = getPointDirection();
        float forwardComponent = oldVelocity.dot(pointDirection);
        forwardComponent *= scene.getScaleFactor(); // convert to m/s
        if (forwardComponent > 0.01f) {
            logger.log(Level.INFO, "forwardComponent = {0}", forwardComponent);
            penetrate(overlappingSpatial);
        } else {
            Shafts shafts = scene.getWorld().getShafts();
            shafts.rebound(spatial);
        }
    }
    // *************************************************************************
    // SimpleSolidControl methods

    /**
     * Method invoked before each physics tick to apply a restorative torque
     * (generated by fletchings or vanes, perhaps) which tends to keep the shaft
     * pointed in the direction of motion.
     *
     * @param space
     * @param unused2
     */
    @Override
    public void prePhysicsTick(PhysicsSpace space, float unused2) {
        assert space == scene.getWorld().getPhysics().getSpace();
        if (spatial == null || !isEnabled() || !scene.isEnabled()) {
            return;
        }
        Vector3f pointDirection = getPointDirection();
        Vector3f velocity = getLinearVelocity();
        Vector3f cross = pointDirection.cross(velocity); // world units / second
        Shafts shafts = scene.getWorld().getShafts();
        float length = shafts.getLength(spatial); // meters
        float scaleFactor = scene.getScaleFactor();
        cross.multLocal(length * mass * restoreGain * scaleFactor);
        applyTorqueImpulse(cross);
        oldVelocity = velocity.clone();
    }
    // *************************************************************************
    // SimpleSolidControl methods

    /**
     * (Re-)Initialize this control each time it is added.
     */
    @Override
    public void onAdd() {
        super.onAdd();

        float scaleFactor = scene.getScaleFactor();
        setCcdMotionThreshold(Physics.ccdMotionThreshold / scaleFactor);

        Shafts shafts = scene.getWorld().getShafts();
        float radius = shafts.getRadius(spatial); // meters
        radius /= scaleFactor; // convert to world units
        setCcdSweptSphereRadius(radius);

        setFriction(friction);
    }
    // *************************************************************************
    // private methods

    /**
     * Get the shaft's +Z direction in world coordinates.
     *
     * @return a new unit vector
     */
    private Vector3f getPointDirection() {
        Quaternion orientation = getPhysicsRotation();
        Vector3f direction = orientation.mult(Vector3f.UNIT_Z);

        assert direction.isUnitVector();
        return direction;
    }

    /**
     * The shaft has entered the collision shape of an obstacle. Transfer
     * control to PenetrateControl.
     *
     * @param obstacle which obstacle caused the transition (not null)
     */
    private void penetrate(Spatial obstacle) {
        assert obstacle != null;
        assert scene.contains(obstacle);
        logger.log(Level.INFO, "obstacle={0}", obstacle.getName());
        /*
         * Transfer control.
         */
        PenetrateControl penetrateControl =
                spatial.getControl(PenetrateControl.class);
        penetrateControl.enable(oldVelocity, obstacle);
        setEnabled(false);
    }
}
